'''
Created on Oct 17, 2012

@author: Zeta
'''
import sys
import pyaudio
import wave
import struct
import math
import blockAnalyzer
from collections import namedtuple
from formatting import *


class RawReaderException(Exception):
    pass

class RawFileReader(object):
    def __init__(self,filePath):
        'Constantes Para obtener el stream de audio'
        self.CHANNELS = '?'
        self.AMPLITUDE_RESOLUTION = '?'
        'Clases'
        self.pa = pyaudio.PyAudio()
        'Stream para escuchar el archivo'
        self.streamOut = ''
        'Archivo'
        self.wf = wave.open(filePath, 'rb')
        self.AMPLITUDE_RESOLUTION = self.pa.get_format_from_width(self.wf.getsampwidth())
        self.CHANNELS = self.wf.getnchannels()
        
    #publico
    def start(self, baseReaderQueMeUsa):
        self.baseReader = baseReaderQueMeUsa
        self.open_stream()
        self.baseReader.available = True
        
    def open_stream(self):
        self.streamOut = self.pa.open(   format = self.AMPLITUDE_RESOLUTION,
                                 channels = self.CHANNELS,
                                 rate = self.baseReader.SAMPLING_FREQUENCY,
                                 #rate = self.wf.getframerate(),
                                 output = True)

    #publico
    def stop(self):
        self.stream.close()
        self.pa.terminate()
    #publico
    def read(self):
        try:
            streamBlock = self.wf.readframes(self.baseReader.NUMBER_OF_SAMPLES)            
        except Exception:
            print( "Error leyendo el archivo" )
            trace = sys.exc_info()[2]
            raise RawReaderException("Error leyendo el archivo")

        count = (len(streamBlock)/2) / self.CHANNELS
        #Escuchar
        if streamBlock == '' or self.baseReader.NUMBER_OF_SAMPLES != count: 
            print(' = = = FIN DE WAV = = = ')
            print(' (repito el ultimo bloque) ')
            self.baseReader.available = False
            return self.baseReader.block.samples
                 
        self.streamOut.write(streamBlock) #TODO> outputear desde el bloque para verificar que se transformo bien

        # We need to convert this string of bytes into a string of 16-bit samples...
        # we will get one short out for each two chars in the string.
        format = "%dh"%(count*self.CHANNELS)
        newBlock = list(struct.unpack( format, streamBlock ))
        # iterate over the block.
        for x in range(count*self.CHANNELS):
            # sample is a signed short int +/- 32768. Normalize it to 1.0
            newBlock[x] = newBlock[x] / 32768.0
        return newBlock
        

class RawMicReader(object):
    def __init__(self):
        'Constantes Para obtener el stream de audio'
        self.CHANNELS = 1
        self.AMPLITUDE_RESOLUTION = pyaudio.paInt16
        'Clases'
        self.pa = pyaudio.PyAudio()
        'Stream'
        self.stream = ''
    
    #publico
    def start(self, baseReaderQueMeUsa):
        self.baseReader = baseReaderQueMeUsa
        self.open_stream()
        self.baseReader.available = True
        
    def open_stream(self):
        device_index = self.find_input_device()
        self.stream = self.pa.open(   format = self.AMPLITUDE_RESOLUTION,
                                 channels = self.CHANNELS,
                                 rate = self.baseReader.SAMPLING_FREQUENCY,
                                 input = True,
                                 input_device_index = device_index,
                                 frames_per_buffer = self.baseReader.NUMBER_OF_SAMPLES)

    def find_input_device(self):
        device_index = None            
        for i in range( self.pa.get_device_count() ):     
            devinfo = self.pa.get_device_info_by_index(i)   
            print( "Device %d: %s"%(i,devinfo["name"]) )
            for keyword in ["mic","input"]:
                if keyword in devinfo["name"].lower():
                    print( "Found an input: device %d - %s"%(i,devinfo["name"]) )
                    device_index = i
                    return device_index
        if device_index == None:
            print( "No preferred input found; using default input device." )
        return device_index
    
    #publico
    def stop(self):
        self.stream.close()
        self.pa.terminate()
    #publico
    def read(self):
        try:
            streamBlock = self.stream.read(self.baseReader.NUMBER_OF_SAMPLES)
            # We need to convert this string of bytes into a string of 16-bit samples...
            # we will get one short out for each two chars in the string.
            count = len(streamBlock)/2
            format = "%dh"%(count)
            newBlock = list(struct.unpack( format, streamBlock ))
            # iterate over the block.
            for x in range(count):
                # sample is a signed short in +/- 32768. Normalize it to 1.0
                newBlock[x] = newBlock[x] / 32768.0
        except IOError:
            print( "Error leyendo el microfono" )
            trace = sys.exc_info()[2]
            raise RawReaderException("Error leyendo el microfono")
        return newBlock



class BaseReader(object):
    def __init__(self, rawReader):
        'Constantes Fundamentales para Bloques. Publicas.'
        self.SAMPLING_FREQUENCY = 44100  #muestras por segundo, en Hz
        self.NUMBER_OF_SAMPLES = 1024   #Avoid zero padding
        
        'Clases'
        self.streamReader = rawReader
        self.analizar = blockAnalyzer.blockAnalyzer()
        
        'Available'
        self.available = False
        self.streamReader.start(self) #available a true
                
        'Struct Bloque. Privado pero se manda por parametro'
        self.block = namedtuple("Block", "samples"+
                                        " SAMPLING_FREQUENCY"+
                                        " NUMBER_OF_SAMPLES"+
                                        " SAMPLING_TIME"+
                                        " SAMPLING_INTERVAL"+
                                        " MAXIMUM_FREQUENCY"+
                                        " FREQUENCY_RESOLUTION")
        #muestras por segundo, en Hz
        self.block.SAMPLING_FREQUENCY = 1.0 * self.SAMPLING_FREQUENCY  
        #Cantidad de muestras en el bloque
        self.block.NUMBER_OF_SAMPLES = 1.0 * self.NUMBER_OF_SAMPLES   
        #SAMPLING_TIME: Tiempo total de muestreo
        self.block.SAMPLING_TIME = 1.0 * self.NUMBER_OF_SAMPLES / self.SAMPLING_FREQUENCY 
        #SAMPLING_INTERVAL: Tiempo entre 2 muestras
        self.block.SAMPLING_INTERVAL = 1.0 / self.SAMPLING_FREQUENCY
        #Frecuencia maxima que podemos "leer"
        self.block.MAXIMUM_FREQUENCY = 1.0 * self.SAMPLING_FREQUENCY / 2
        #Hz de diferencia entre cada frecuencia detectada
        self.block.FREQUENCY_RESOLUTION = 1.0 * self.SAMPLING_FREQUENCY / self.NUMBER_OF_SAMPLES
                                        
        'Variables'
        self.block.samples = [0]
        self.error = True

    def stop(self):
        self.streamReader.stop()
        self.block.samples = [0]
        self.error = True
        
        
    

    def reportate(self):

        print("AMPLITUDE_RESOLUTION (bits por muestra) = " + str(type( pyaudio.paInt16 )))
        print("SAMPLING_FREQUENCY (muestras por segundo, en Hz) = " + str(self.block.SAMPLING_FREQUENCY))
        print("NUMBER_OF_SAMPLES (Avoid zero padding) = " + str(self.block.NUMBER_OF_SAMPLES))
        print("SAMPLING_TIME (Tiempo total de muestreo) = " + str(self.block.SAMPLING_TIME ))
        print("SAMPLING_INTERVAL (Tiempo entre 2 muestras) = " + str(self.block.SAMPLING_INTERVAL))
        print("MAXIMUM_FREQUENCY (Frecuencia maxima que podemos ""leer"") = " + str(self.block.MAXIMUM_FREQUENCY))
        print("FREQUENCY_RESOLUTION (Hz de diferencia entre cada frecuencia detectada) = " + str(self.block.FREQUENCY_RESOLUTION)) 
        if self.available:
            print("= = = LISTO PARA ESCUCHAR = = =")
        else:
            print("= = = !! NO DISPONIBLE !! = = =")
        
    def getNewBlock( self ):
        if self.available == False:
            self.block.samples = [0]
            print('Se acabo el String no me pidas mas bloques')
            return
        try:
            self.error = False
            self.block.samples = self.streamReader.read()
        except RawReaderException:
            # el printing lo hace el streamReader
            self.error = True
            self.block.samples = [0]


class TapTester(BaseReader):
    def __init__(self, rawReader):
        super(TapTester,self).__init__(rawReader)
        'Constantes'
        self.INITIAL_TAP_THRESHOLD = 0.200
 
        # if we get this many noisy blocks in a row, increase the threshold
        self.OVERSENSITIVE = 15.0/self.block.SAMPLING_TIME                    
        # if we get this many quiet blocks in a row, decrease the threshold
        self.UNDERSENSITIVE = 120.0/self.block.SAMPLING_TIME 
        # if the noise was longer than this many blocks, it's not a 'tap'
        self.MAX_TAP_BLOCKS = 0.15/self.block.SAMPLING_TIME
 
        'Variables'
        self.tap_threshold = self.INITIAL_TAP_THRESHOLD
        self.noisycount = self.MAX_TAP_BLOCKS+1 
        self.quietcount = 0 
        self.errorcount = 0

    def tapDetected(self):
        print("Tap!")
        print(showAsPorcentaje( self.analizar.get_FFT_440( self.block ))) 

    def listen(self):
        self.getNewBlock()
        if self.error:
            # dammit. 
            self.errorcount += 1
            self.noisycount = 1
            return

        amplitude = self.analizar.get_rms( self.block )
        if amplitude > self.tap_threshold:
            # noisy block
            self.quietcount = 0
            self.noisycount += 1
            if self.noisycount > self.OVERSENSITIVE:
                # turn down the sensitivity
                self.tap_threshold *= 1.1
        else:            
            # quiet block.

            if 1 <= self.noisycount <= self.MAX_TAP_BLOCKS:
                self.tapDetected()
            self.noisycount = 0
            self.quietcount += 1
            if self.quietcount > self.UNDERSENSITIVE:
                # turn up the sensitivity
                self.tap_threshold *= 0.9


class SecondBySecond(BaseReader):
    def __init__(self, rawReader):
        super(SecondBySecond,self).__init__(rawReader)
        'Constantes'
        self.REPORTES_POR_SEGUNDO = 50.0
        'Variables'
        self.timeCounter = 0.0
 
 
    def listen(self):
        self.getNewBlock()
        self.timeCounter = self.timeCounter + self.block.SAMPLING_TIME
        if self.error:
            # dammit.
            if self.timeCounter >= (1.0 / self.REPORTES_POR_SEGUNDO):
                self.timeCounter = 0.0 
            return
        if self.timeCounter >= 0.0:
            self.expone()
            self.timeCounter = self.timeCounter - (1.0 / self.REPORTES_POR_SEGUNDO)
        
    def expone(self):
        print ('RMS: ' +
            showAsPorcentaje(self.analizar.get_rms( self.block )) +
            '  ' +
            'Freq 1700:' +
            showAsPorcentaje( self.analizar.get_FFT_Frequency( self.block, 1700 )) +
            '  ' +
            'Freq 1760:' +
            showAsPorcentaje( self.analizar.get_FFT_Frequency( self.block, 1760 )) +
            '  ' +
            'Freq 1820:' +
            showAsPorcentaje( self.analizar.get_FFT_Frequency( self.block, 1820 )) +
            '   __' + str(self.analizar.get_frecuencia_notas(1760)))

class SoundReader(BaseReader):
    def __init__(self, rawReader):
        super(SoundReader,self).__init__(rawReader)
        'Constantes'
        #Relacion entre la amplitud de la primera y la segunda frecuencia mas predominante
        #Para que se considere al tono como reconocido
        self.RELATIVE_SENSITIVITY = 1.4    #X*2 equivale a X + 3 Decibeles:
        self.AMP_SENSITIVITY = 0.05    #Entre 0 y 1 potencia necesaria para aceptar el tono
        self.FREQ_CODE_BASE = 1760    #Los codigos empiezan con el LA bastante agudo
        'Clases'
        self.decoder =  blockAnalyzer.DeCodificador()
        self.CODE_FREQ_ARRAY = self.analizar.get_frecuencia_notas(self.FREQ_CODE_BASE)
        self.CODE_AMP_ARRAY = [0,0,0,0,0,0,0,0,0,0,0,0,0]
 
    def imprimirMensaje(self):
        print('')
        print('Archivo Enviado:')
        self.decoder.imprimirArchivo()
 
    def reportate(self):
        super(SoundReader,self).reportate()
        print ('Frecuencia de codigos empezando por el 0: ' +
               str(self.CODE_FREQ_ARRAY))
        
    def listen(self):
        if not self.available: return
        self.getNewBlock()
        #popular mi array de codigos con amplitudes
        for x in range(0,10):
            self.CODE_AMP_ARRAY[x] = (
                self.analizar.get_FFT_Frequency(self.block, self.CODE_FREQ_ARRAY[x]) )
        #buscar el codigo mas fuerte y el 2do mas fuerte
        codMasFuerte = -1.0
        valMasFuerte = -1.0
        for x in range(0,10):
            if self.CODE_AMP_ARRAY[x] > valMasFuerte:
                valMasFuerte = self.CODE_AMP_ARRAY[x]
                codMasFuerte = x
        valSegundito = -1.0
        for q in range(0,10):
            if self.CODE_AMP_ARRAY[q] > valSegundito and q != codMasFuerte:
                valSegundito = self.CODE_AMP_ARRAY[q]
        #alimentar al decodificador solo si se supera el threshold configurado
        if valMasFuerte >= 1.0 * valSegundito * self.RELATIVE_SENSITIVITY:
            if valMasFuerte >= self.AMP_SENSITIVITY:
                self.decoder.push(codMasFuerte)
                print(str(codMasFuerte)+' - '+showArrayAsPorcentaje(self.CODE_AMP_ARRAY))
            else:
                print('?' +' - '+showArrayAsPorcentaje(self.CODE_AMP_ARRAY))
        else:
            print('_' +' - '+showArrayAsPorcentaje(self.CODE_AMP_ARRAY))
